# error codes:
# 1 - timeout 
# 2 -  tcp errors
# 3 - dns error. can't resolve name and etc. (see EV::ADNS)
# 4- can't create socket
# 5 - reserved fo future use
# 6 - something wrong with socks server 
# 7 - authentification error  - identd could not confirm the user ID 
# 8 - identd  not reachable 
# 9 - address type is not implemented (probably ip6)
package Net::Coro::EV::Connect::Socks4;
use common::sense;
our $VERSION = 0.01;

use Socket;
use EV;
use Net::Coro::EV::Connect::Socket;

our $TRACE = 0;
our $TIMEOUT = 20;

sub new {
	my ($class, $sckhost, $sckport, $socket) =@_;
	
	my $this ={};
	
	$socket = new Net::Coro::EV::Connect::Socket if !$socket;
	$this->{_SOCKET} = $socket;

	$this->{_SCKHOST} = $sckhost; # socks host
	$this->{_SCKPORT} = $sckport; # socks port
	$this->{_TIMEOUT} = $TIMEOUT;
	
	bless $this, $class;
	return $this;
}
sub type {'socks4'}
sub child {
	my ($this) = @_;
	my $child = {};
	bless $child, ref $this;
		
	while (my ($key, $val) = each(%$this)) {
		$child->{$key} = $val;
	}
	$child->{_SOCKET} = $this->{_SOCKET}->child;
	delete $child->{_CALLER};
	return $child;
}

sub auth {
	my ($this, $name) = @_;
	$this->{_NAME} = $name; # ident id
}

sub address {
	my ($this, $sckhost, $sckport) = @_;
	if (defined $sckhost) {
		$this->{_SCKHOST} = $sckhost; # socks host
		$this->{_SCKPORT} = $sckport; # socks port
	}
	return ($this->{_SCKHOST}, $this->{_SCKPORT});
}


sub connect {
	my ($this, $host, $port, $caller) = @_;
	die("error: socks4 connect to $host:$port. current: state=".$this->{_STATE}.", type=".$this->{_TYPE}) if defined $this->{_WIO};
	utf8::downgrade($host);
	utf8::downgrade($port);
	
	$this->{_CALLER} = $caller;
	$this->{_HOST} = $host;
	$this->{_PORT} = $port;

	$this->{_SOCKET}->{_DNS}->resolve($host, $port, $this);
}

sub onresolve {	
	my ($this, $ip, $port, $is6) = @_;
	return $this->err($this->{_SOCKET}->{_DNS}->errm, $this->{_SOCKET}->{_DNS}->errc) if !$ip;
	return $this->err("socks4: doesn't support ipv6 (resolved host $this->{_HOST}) ", 9) if $is6;
	
	$this->{_HOST} = $ip;
	print "TRACE $this->{_ID}, Socks4 connection begining - $this->{_SCKHOST}:$this->{_SCKPORT}\n" if $TRACE;
	$this->{_SOCKET}->connect($this->{_SCKHOST}, $this->{_SCKPORT}, $this);
}

sub onconnect {
	my ($this, $wio, $wtm, $serv_ip, $serv_port) = @_;
	return $this->err($this->{_SOCKET}->errm, $this->{_SOCKET}->errc) if !$wio;
	
	$this->{_WIO} = $wio;
	$this->{_WTM} = $wtm;
	
	$this->{_WIO}->data($this);
	$this->{_WIO}->events (EV::WRITE);
}


sub onwrite {
	my ($this) = @_;
	
	my $name = (defined $this->{_NAME})?$this->{_NAME}:"";
	my $request = pack("CCnA4", 4, 1, $this->{_PORT}, inet_aton($this->{_HOST})).pack("C*", $name).pack("x");
	return $request;
}


sub onread {
	my ($this, $response) = @_;	# plain text
	
		# check first socks response
		my ($null, $code, $rest) = unpack("C2a*", $response);
		
		if ($code == 90) { #0x5a = request granted
			$this->callback(delete($this->{_WIO}),delete($this->{_WTM}));
			return;
		}
		
		if ($code == 91) {#    0x5b = request rejected or failed
			$this->err("socks4: request rejected or failed",6);
		} elsif ($code == 92) { #     0x5c = request failed because client is not running identd (or not reachable from the server)
			$this->err("socks4: request failed because client is not running identd (or not reachable from the server)",8);
		} elsif ($code == 93) { #   0x5d = request failed because client's identd could not confirm the user ID string in the request
			$this->err("socks4: request failed because client's identd could not confirm the user ID string in the request",7);
		} else {
			$this->err("socks4: unexpected response ".chr($null).chr($code).$rest, 6);
		}
}


######## COMMON ARCHITECTURAL METHODS AND EVENT HANDLERS ###################
sub onwritten {
	my ($this, $w) = @_;
	$w->events (EV::READ);
}

sub onclose {
	my ($this) = @_;
	# make desigion what to do
	# default is to send error
	$this->err("socks4: connectnion closed by peer",6);
}

sub onbreak {
	my ($this, $msg, $code) = @_;
	# make desigion what to do
	# default is to send error
	$this->err($msg, 2);
}

sub callback {
	my ($this, @resp) = @_;
	undef $this->{_ERRM};
	undef $this->{_ERRC};
	print "TRACE $this->{_ID}, Socks4 connection done \n" if $TRACE;
	(delete $this->{_CALLER})->onconnect(@resp);
}

sub err {
	my ($this, $msg, $code) = @_;
	$this->{_ERRM} = $msg;
	$this->{_ERRC} = $code;

	print "TRACE $this->{_ID}, Socks4 connection error - $msg (code $code)\n" if $TRACE;
	$this->close;
	(delete $this->{_CALLER})->onconnect();
}


sub close {
	my ($this) =@_;
	
	my $tm = delete $this->{_WTM};
	$tm->stop() if defined $tm;
	
	my $w = delete $this->{_WIO};
	if (defined $w) {
		$w->stop();
		my $fh = $w->fh;
		shutdown($fh, 2);
		CORE::close($fh);
	}
}

sub stop {
	my ($this) = @_;
	$this->{_SOCKET}->stop();
	$this->close();
	delete $this->{_CALLER};
}



############ COMMON PROPIRTIES ##############
sub errc {$_[0]->{_ERRC}}
sub errm {$_[0]->{_ERRM}}

sub timeout {
	my ($this, $val) = @_;	
	if (defined $val) {
		$this->{_TIMEOUT} = $val;
		$this->{_SOCKET}->timeout($val);
	}
	return $this->{_TIMEOUT};
}

sub bindip {$_[0]->{_SOCKET}->bindip($_[1])}

sub id {
	my ($this, $id) = @_;
	if (defined $id) {
		$this->{_ID} = $id;
		$this->{_SOCKET}->id($id);
	}
	$this->{_ID};
}
1;